/*  
  -- The Lime II Project -- 

  A tuplespaces-based middleware for coordinating agents and mobile hosts.
  Copyright (C) 2005.
  Gian Pietro Picco, Amy L. Murphy, Lorenzo Bellini.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  Lesser General Public License for more details.

  You should have received this copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
*/

package lime2;
import lime2.ca.ICommunicationAdapter;
import lights.adapters.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;

/**
 * Runtime support of Lime: models a host.
 * <br>
 * LimeServer is a singleton: there is one LimeServer per JVM; thus our convention is that <i>host</i> means 
 * LimeServer; we will make it explicit when we use <i>host</i> to refer to the machine the LimeServer runs on top of
 * (as in the case of multiple LimeServers on localhost).  
 * <br>
 * Its primary task is to load <i>agents</i> (an agent is a class implementing the Runnable interface).
 * <br><br>
 * <b>Notes for Proper Usage</b>
 * <br>
 * &#9679 the convention is that the port the LimeServer uses for loading agents (<i>load port</i>) is
 * <i>unicast port + 1</i> 
 * <br>
 * &#9679 apart from <code>getServer</code> that can be called at any time, the other <code>get..</code> methods of
 * LimeServer should be called only after that the LimeServer has been booted. If the LimeServer has not been booted, 
 * you can access the corresponding data from Properties <code>settings</code> (but at least <code>settings</code> 
 * must be already initialized according to the will of the user, by the Launcher or another application)    
 * 
 * @author Gian Pietro Picco
 * @author Amy Murphy
 * @author Lorenzo Bellini
 */

public class LimeServer implements PropertyKeys, Runnable
{
 // the default settings of the middleware
 private static Properties defaultSettings = new Properties();
 
 /** 
  * The settings of the middleware.
  */
 public static Properties settings = new Properties(defaultSettings);		
		
 // multicast address of the LimeServer	
 private String mcastaddr = null;  
 
 // multicast port of the LimeServer
 private int mcastport = -1; 
 
 // unicast address of the LimeServer
 private InetAddress ucastaddr = null;
 
 // primary unicast port of the LimeServer
 private int ucastport = -1;  

 // secondary unicast port of the LimeServer: it is used for loading agents (= ucastport + 1)
 private int loadport = -1;

 // http diagnostics port of the LimeServer
 private int http_port = -1;

 // unicast LimeServerID of the LimeServer (the primary ID of the LimeServer)
 private LimeServerID ucastID = null;
 
 // multicast LimeServerID of the LimeServer
 private LimeServerID mcastID = null;

 // tuplespace factory used by the LimeServer
 private String tsfactory = null;
 
 // communication adapter factory used by the LimeServer
 private String cafactory = null;

 // has the LimeServer been booted ?
 private boolean booted = false;
 
 // is the LimeServer engaged ?
 private boolean engaged = false;
 
 // the LimeServer singleton
 private final static LimeServer server = new LimeServer();

 // the constructor of the LimeServer: it is private since LimeServer is a singleton
 private LimeServer() 
 {	
  super();
  
  // this is the only LimeServer variable that cannot be set by user: we initialize it now
  try { ucastaddr = InetAddress.getLocalHost(); }
  catch (UnknownHostException uhe) { throw new LimeError("can't resolve this host", uhe); }
 }

 /**
  * Activates the runtime support. 
  * <br>
  * LimeServer is fully operative only after that this method has been invoked.
  * 
  * @throws BootException
  */
 synchronized public void boot() 
 {	 
  // the server can be booted only once
  if (booted) throw new BootException("server has already been booted");	
 	
  // the boot method must be called only after that "settings" have been set (see main method of Launcher).
  // "settings" should not be modified after that the LimeServer has been booted 
  applySettings();	

  // creates multicast and unicast LimeServerIDs; initializes Location.HERE	
  try 
  {
   mcastID = new LimeServerID(InetAddress.getByName(mcastaddr), mcastport);	
   ucastID = new LimeServerID(ucastaddr, ucastport);
   Location.initHere(ucastID);
  }
  catch (UnknownHostException uhe) { throw new LimeError("can't create Lime ID(s)", uhe); }	
 	
  // initializes the communication manager
  try { CommunicationMgr.build(cafactory); }
  catch(LimeCommunicationException lce) { throw new LimeError("can't build C-Adapter", lce); }
  
  // sets the tuplespace factory
  try { TupleSpaceFactory.setFactory(tsfactory); } 
  catch(ClassNotFoundException cnfe) { throw new LimeError("can't set T-Factory", cnfe); }
  
  // booting sequence "nearly" finished 
  booted = true;
  
  // threading on this object; "booted" must be true when agents are loaded, otherwise the very first ones will not get
  // handles to LimeTupleSpaces - see get method of LimeTupleSpace
  new Thread(this).start();

  // prints welcome: the user can recognise that at this point the LimeServer is ready (..apart from LimeHTTP)
  System.out.println("C-Adapter set to " + cafactory);
  System.out.println("T-Factory set to " + tsfactory);
  System.out.println("LimeServer <" + ucastID + "> <" + mcastID + "> activated");
 
  // the thread of LimeHTTP must be started only at the very end of the booting sequence, since LimeHTTP needs
  // to invoke the "get" methods of LimeServer and the "get" method of LimeTupleSpace; the LimeHTTP server runs 
  // only if http diagnostics has been enabled 
  if (http_port != -1) 
  {
   new Thread(new LimeHTTP(http_port)).start();
   System.out.println("HTTP diagnostics service running");
  }
  
  // if the auto-engage option is set, then the LimeServer must engage on startup
  if (new Boolean(settings.getProperty(DO_ENGAGE_KEY)).booleanValue()) 
	  engage();
 }
 
 /**
  * Run method - continuously waits for requests to load agents. 
  * <br><br>
  * This service:
  * <P>
  * &#9679 is lightweight since it doesn't accept multiple connections
  * <br>
  * &#9679 delegates concrete loading of the agent to <code>loadAgent(String, Object[])</code>
  * <br>
  * &#9679 allows programs that are <i>remote</i> to execute an agent within this LimeServer (where <i>remote</i> means
  * running on another JVM wrt the JVM that runs the LimeServer) 
  * </P>
  * The <u>protocol</u> for requesting the LimeServer to load an agent is very simple; after that the application has 
  * connected (using TCP/IP) to the load port of the LimeServer (= unicast port + 1), it has to: 
  * <P><code>
  * &#9679 send the (fully qualified) name of the class of the agent (<b>StringUTF</b>)
  * <br>
  * &#9679 send the number of parameters of the constructor of the agent (<b>int</b>)
  * <br>
  * &#9679 send each parameter one after the other (<b>Serializable object</b>)
  * </code></P>
  * This is not code mobility: the bytecode of the class of the agent is not dynamically retrieved from the connection.
  * Applications can ask an host only to load agents that belong to the codebase of the host itself. This enforces a
  * strong security policy: administrators of the server <i>know</i> the behavior of the agents. 
  * Supporting true code mobility would be easy (redefine the Java ClasseLoader; see example of NetworkClassLoader in
  * ClassLoader javadoc), but many security problems would arise: since agents are just Runnable entities, we should 
  * prevent an agent from being able to execute malicious code on our host.
  * <br>
  * For the sake of network congestion and performance of this service (which is not parallel - a new request is 
  * processed only when the previous one has terminated), the Serialized parameters should be <i>small</i>.
  * </P>
  */
 public void run()
 {
  // initializes the ServerSocket (if it fails the system is halted)
  ServerSocket loadSocket = null;
  try { loadSocket = new ServerSocket(loadport); } 
  catch (IOException ioe) { throw new LimeError("can't listen on load port (" + loadport + ")", ioe); }

  // client socket && stream
  Socket s = null;	
  ObjectInputStream ois = null;
  
  //loop
  for(;;) 
  {
   // auxiliary variables	
   String classname = null;
   Object[] pars = null; 
   int npars = 0;
    
   try 
   {
	s = loadSocket.accept();
    ois = new ObjectInputStream(s.getInputStream());
	classname = ois.readUTF();
	npars = ois.readInt();
	if (npars > 0) 
	{ 
	 pars = new Object[npars]; 	
	 try { for (int i = 0; i < npars; i++) pars[i] = ois.readObject(); }
	 catch (ClassNotFoundException cnfe) { throw new AgentCreationException("can't find the agent class", cnfe); }
   	}	
	loadAgent(classname, pars);
	ois.close();
	s.close();
   }
   catch (AgentCreationException ace) { System.out.println("agent " + classname + " exception: " + ace.getMessage()); } 
   catch (IOException ioe) { ioe.printStackTrace(); } // just prints stack trace: "robust" server 
  } 
  // end loop 	
 }

 /**
  * Loads an agent into the Lime server. 
  * <br>
  * An agent is just a Runnable class: when the LimeServer is requested to load a specific agent, it creates an 
  * instance of the agent and start a new thread on it. Thus the "run" method of the agent is executed within the 
  * LimeServer. 
  * <br>
  * Please note that, on the basis of the supplied parameters, the correct constructor (must be public) is 
  * automatically retrieved by its signature . If parameters are <code>null</code> or an array of length zero the 
  * empty constructor is invoked (it can be the implicit empty constructor - pay attention: if you defined also non 
  * empty constructors for the class of the agent, the implicit empty constructor is destroyed, you will have to 
  * explicitly define it). An empty constructor does not allow flow of data: it is not possible to choose the initial
  * state of the agent.
  * <br>
  * This method is invoked by the <code>run</code> method; also it can be invoked by <i>local</i> applications (where
  * <i>local</i> means running in the same JVM of this LimeServer).
  *
  * @param classname the fully specified name of the agent's class.
  * @param parameters array of Objects, the parameters for instantiating the agent
  * @throws AgentCreationException if it is impossible to create the agent.
  */
 public void loadAgent(String classname, Object[] parameters) throws AgentCreationException
 {
  Runnable a = null;
  
  try 
  {
   Class agentClass = Class.forName(classname);
   if (!(Runnable.class).isAssignableFrom(agentClass)) throw new AgentCreationException("agent doesn't implement Runnable");
   if (parameters == null || parameters.length == 0) a = (Runnable) agentClass.newInstance();
   else 
   {
    Class[] parTypes = new Class[parameters.length];
 	for (int i = 0; i < parameters.length; i++) parTypes[i] = parameters[i].getClass();
    Constructor constr = agentClass.getConstructor(parTypes);
	a = (Runnable) constr.newInstance(parameters);
   }
   new Thread(a).start();
   System.out.println("agent " + classname + " loaded");
  } 
  catch (ClassNotFoundException e) { throw new AgentCreationException("can't find some of the agent classes", e); } 
  catch (IllegalAccessException e) { throw new AgentCreationException("can't load some of the agent classes\n(are they all public ?)", e); } 
  catch (InstantiationException e) { throw new AgentCreationException("can't instantiate the agent object", e); } 
  catch (NoSuchMethodException  e) { throw new AgentCreationException("no constructor matching the parameters found", e); } 
  catch (InvocationTargetException e) { throw new AgentCreationException("constructor threw exception\n" + (e.getMessage() != null ? ".. " + e.getMessage().toString() : ""), e); }
  catch (LinkageError e) { throw new AgentCreationException("can't link class\n" + (e.getMessage() != null ? ".. " + e.getMessage().toString() : ""), e); }
 }

 /**
  * Engages the LimeServer: all the tuplespaces will be shared.
  */
 synchronized public void engage()
 {
  if (!booted) throw new BootException("server has not been booted");		
  if (!engaged)
  {	
   ICommunicationAdapter ica = CommunicationMgr.get().getCA();	
   ica.joinSystem(null);
   String[] groups = LimeTupleSpace.getNames();
   for (int i = 0; i < groups.length; i++) ica.joinGroup(groups[i]);  
   engaged = true;
  }
 }
 
 /**
  * Disengages the LimeServer: all the tuplespaces will be unshared.
  */
 synchronized public void disengage()
 {
  if (!booted) throw new BootException("server has not been booted");		
  if (engaged)
  {	
   ICommunicationAdapter ica = CommunicationMgr.get().getCA();	
   String[] groups = LimeTupleSpace.getNames();
   for (int i = 0; i < groups.length; i++) ica.leaveGroup(groups[i]);  
   ica.leaveSystem();
   engaged = false;
  }
 }
 
 /**
  * Shuts-down gracefully (= disengagement) the LimeServer.
  */
 synchronized public void shutdown()
 {
  System.out.print("Shutting down... ");	
  // .. if engaged, disengage
  if (engaged) disengage();
  // .. deactivates the communication layer
  CommunicationMgr.get().getCA().shutdown();
  // .. please note that running agents are simply interrupted since we cannot guarantee that they will terminate
  // their tasks in a reasonable amount of time. Also: we could stop enqueuing new ops and wait for all the enqueued 
  // ops to be processed but still we could not give an upper bound for the "length" of the queue 
  System.out.println("done");
  System.exit(0);
 }
 
 synchronized public void shutdown(boolean exit) {
	 System.out.print("Shutting down... ");	
	  // .. if engaged, disengage
	  if (engaged) disengage();
	  // .. deactivates the communication layer
	  CommunicationMgr.get().getCA().shutdown();
	  // .. please note that running agents are simply interrupted since we cannot guarantee that they will terminate
	  // their tasks in a reasonable amount of time. Also: we could stop enqueuing new ops and wait for all the enqueued 
	  // ops to be processed but still we could not give an upper bound for the "length" of the queue 
	  System.out.println("done");
	  if(exit) System.exit(0);
 }
 
 /**
  * Returns the (singleton) LimeServer. 
  * <br>
  * Always returns a non <code>null</code> object, since the LimeServer singleton is statically initialized; but the
  * LimeServer could be not booted.
  * 
  * @return LimeServer
  */ 
 public static LimeServer getServer() { return server; }

 /**
  * Returns whether the LimeServer has been booted or not.
  * 
  * @return boolean
  */
 public boolean isBooted() { return booted; } 
 
 /**
  * Returns whether the LimeServer is engaged or not.
  * 
  * @return boolean
  */
 public boolean isEngaged() { return engaged; }
 
 /**
  * Returns the multicast address of the LimeServer.
  * 
  * @return multicast address as a String
  */
 public String getMulticastAddress() { return mcastaddr; }
 
 /**
  * Returns the multicast port of the LimeServer.
  * 
  * @return multicast port  
  */
 public int getMulticastPort() { return mcastport; }
 
 /**
  * Returns the unicast address of the LimeServer.
  * 
  * @return unicast address 
  */
 public InetAddress getUnicastAddress() { return ucastaddr; }
 
 /**
  * Returns the primary unicast port of the LimeServer.
  * 
  * @return unicast port
  */
 public int getUnicastPort() { return ucastport; }
 
 /**
  * Returns the unicast port of the LimeServer that is used for loading agents.
  * 
  * @return port for loading agents
  */
 public int getLoadPort() { return loadport; }
 
 /**
  * Returns the http diagnostics port of the LimeServer.
  * 
  * @return port for http diagnostics
  */
 public int getDiagnosticsPort() { return http_port; }
 
 /**
  * Returns the tuplespace factory that the LimeServer (and its agents/applications) use. 
  *
  * @return name of the tuplespace factory
  */
 public String getTSFactory() { return tsfactory; }
 
 /**
  * Returns the communication adapter factory of the LimeServer.
  * 
  * @return name of the communication adapter
  */
 public String getCAdapter() { return cafactory; }
 
 /**
  * Returns the multicast LimeServerID of the LimeServer.
  * 
  * @return multicast LimeServerID
  */
 public LimeServerID getMulticastID() { return mcastID; }
 
 /**
  * Returns the unicast LimeServerID of the LimeServer.
  * 
  * @return unicast LimeServerID
  */
 public LimeServerID getServerID() { return ucastID; }

 /**
  * Shuts-down brutally the LimeServer. 
  *
  * @param le occurred LimeError 
  */
 public void fail(LimeError le)
 {
  System.out.println("Fatal error: " + (le.getMessage() != null ? (le.getMessage() + "\n             ") : "") +
  		             ((le.getCause() != null && le.getCause().getMessage() != null) ? le.getCause().getMessage() : "no description"));
  System.exit(-1);
 }
 
 // an auxiliary method that loads the (chosen) "settings" into the corresponding fields of the LimeServer singleton
 private void applySettings()
 {
  mcastaddr = stringProperty(MCASTADDR_KEY);
  mcastport = intProperty(MCASTPORT_KEY);
  ucastport = intProperty(UCASTPORT_KEY);
  http_port = intProperty(HTTP_PORT_KEY);
  tsfactory = stringProperty(TSFACTORY_KEY);
  cafactory = stringProperty(CAFACTORY_KEY);
  loadport  = ucastport + 1;
 }
 
 // initialization of the default settings
 static 
 {
  // user-level properties	
  defaultSettings.setProperty(MCASTADDR_KEY, MCASTADDR_VAL);
  defaultSettings.setProperty(MCASTPORT_KEY, MCASTPORT_VAL);
  defaultSettings.setProperty(UCASTPORT_KEY, UCASTPORT_VAL);	
  defaultSettings.setProperty(TSFACTORY_KEY, TSFACTORY_VAL);
  defaultSettings.setProperty(CAFACTORY_KEY, CAFACTORY_VAL);
  defaultSettings.setProperty(HTTP_PORT_KEY, HTTP_PORT_VAL);
  defaultSettings.setProperty(DO_ENGAGE_KEY, DO_ENGAGE_VAL);
  defaultSettings.setProperty(DO_SEEMSG_KEY, DO_SEEMSG_VAL);
  // system-level properties
  defaultSettings.setProperty(FRAGM_OUT_KEY, FRAGM_OUT_VAL);
  defaultSettings.setProperty(DEFRAG_IN_KEY, DEFRAG_IN_VAL);
  defaultSettings.setProperty(FRAGS_SIZ_KEY, FRAGS_SIZ_VAL);
  defaultSettings.setProperty(FRAGS_RTM_KEY, FRAGS_RTM_VAL);	
  defaultSettings.setProperty(FRAGS_DFS_KEY, FRAGS_DFS_VAL);
  defaultSettings.setProperty(FRAGS_OVR_KEY, FRAGS_OVR_VAL);
  defaultSettings.setProperty(THREADSPL_KEY, THREADSPL_VAL);
  defaultSettings.setProperty(CACHE_DUP_KEY, CACHE_DUP_VAL);
  defaultSettings.setProperty(CACHE_SIZ_KEY, CACHE_SIZ_VAL);
  defaultSettings.setProperty(CA_RETRYF_KEY, CA_RETRYF_VAL);
  defaultSettings.setProperty(DETECT_PH_KEY, DETECT_PH_VAL);
  defaultSettings.setProperty(MESH_BROADCAST_PH_KEY, MESH_BROADCAST_PH_VAL);
  defaultSettings.setProperty(DELETE_TM_KEY, DELETE_TM_VAL);
  defaultSettings.setProperty(RFRSH_MAX_KEY, RFRSH_MAX_VAL);
  defaultSettings.setProperty(ADV_PHASE_KEY, ADV_PHASE_VAL);
  defaultSettings.setProperty(NOL_TIMER_KEY, NOL_TIMER_VAL);
  defaultSettings.setProperty(NOL_SLEEP_KEY, NOL_SLEEP_VAL);
  defaultSettings.setProperty(UDP_BUFFR_KEY, UDP_BUFFR_VAL);	
  defaultSettings.setProperty(STD_TIMER_KEY, STD_TIMER_VAL);
  defaultSettings.setProperty(FACT_INGG_KEY, FACT_INGG_VAL);
  defaultSettings.setProperty(PACKED_SZ_KEY, PACKED_SZ_VAL);
  defaultSettings.setProperty(CACHE_TCP_KEY, CACHE_TCP_VAL);
  defaultSettings.setProperty(KEEPA_TCP_KEY, KEEPA_TCP_VAL);
  defaultSettings.setProperty(NAME_LMAX_KEY, NAME_LMAX_VAL);
  defaultSettings.setProperty(FORCE_GWR_KEY, FORCE_GWR_VAL);
  defaultSettings.setProperty(LSYS_NAME_KEY, LSYS_NAME_VAL);
  defaultSettings.setProperty(LSYS_POLC_KEY, LSYS_POLC_VAL);
 } 	
 
 // returns a property with the given name as a string 
 static String stringProperty(String x) { return settings.getProperty(x); }
 
 // returns a property with the given name as a long 
 static long longProperty(String x) { return Long.valueOf(settings.getProperty(x)).longValue(); }
 
 // returns a property with the given name as an int  
 static int intProperty(String x) { return Integer.valueOf(settings.getProperty(x)).intValue();	}
 
 // returns a property with the given name as a boolean 
 static boolean booleanProperty(String x) { return Boolean.valueOf(settings.getProperty(x)).booleanValue(); }
 
 // returns a property with the given name as a byte 
 static byte byteProperty(String x) { return Byte.valueOf(settings.getProperty(x)).byteValue();	}
 
}
